জাভাস্ক্রিপ্টে ডোমেইন-ড্রিভেন ডিজাইনে দক্ষতা অর্জন করুন। শক্তিশালী ডোমেইন অবজেক্ট মডেলসহ স্কেলেবল, টেস্টেবল এবং রক্ষণাবেক্ষণযোগ্য অ্যাপ্লিকেশন তৈরি করতে মডিউল এনটিটি প্যাটার্ন শিখুন।
জাভাস্ক্রিপ্ট মডিউল এনটিটি প্যাটার্নস: ডোমেইন অবজেক্ট মডেলিংয়ের একটি গভীর বিশ্লেষণ
সফটওয়্যার ডেভেলপমেন্টের জগতে, বিশেষ করে গতিশীল এবং সদা পরিবর্তনশীল জাভাস্ক্রিপ্ট ইকোসিস্টেমে, আমরা প্রায়শই গতি, ফ্রেমওয়ার্ক এবং ফিচারের উপর বেশি অগ্রাধিকার দিই। আমরা জটিল ইউজার ইন্টারফেস তৈরি করি, অগণিত এপিআই (API)-এর সাথে সংযোগ স্থাপন করি এবং দ্রুত গতিতে অ্যাপ্লিকেশন ডেপ্লয় করি। কিন্তু এই তাড়াহুড়োতে, আমরা কখনও কখনও আমাদের অ্যাপ্লিকেশনের মূল ভিত্তি, অর্থাৎ বিজনেস ডোমেইনকে অবহেলা করি। এর ফলে প্রায়শই "বিগ বল অফ মাড" (Big Ball of Mud) নামক পরিস্থিতির সৃষ্টি হয়—এমন একটি সিস্টেম যেখানে বিজনেস লজিক ছড়ানো-ছিটানো থাকে, ডেটা অসংগঠিত থাকে এবং একটি সাধারণ পরিবর্তনও অপ্রত্যাশিত বাগের কারণ হতে পারে।
এইখানেই ডোমেইন অবজেক্ট মডেলিংয়ের গুরুত্ব। এটি আপনার কাজের সমস্যা ক্ষেত্রের একটি সমৃদ্ধ এবং প্রকাশযোগ্য মডেল তৈরি করার অভ্যাস। এবং জাভাস্ক্রিপ্টে, মডিউল এনটিটি প্যাটার্ন (Module Entity Pattern) এটি অর্জনের একটি শক্তিশালী, মার্জিত এবং ফ্রেমওয়ার্ক-নিরপেক্ষ উপায়। এই বিস্তারিত নির্দেশিকা আপনাকে এই প্যাটার্নের তত্ত্ব, অনুশীলন এবং সুবিধাগুলোর মধ্য দিয়ে নিয়ে যাবে, যা আপনাকে আরও শক্তিশালী, স্কেলেবল এবং রক্ষণাবেক্ষণযোগ্য অ্যাপ্লিকেশন তৈরি করতে সক্ষম করবে।
ডোমেইন অবজেক্ট মডেলিং কী?
প্যাটার্নটিতে প্রবেশ করার আগে, আসুন আমরা কিছু পরিভাষা স্পষ্ট করে নিই। এই ধারণাটিকে ব্রাউজারের ডকুমেন্ট অবজেক্ট মডেল (DOM) থেকে আলাদা করা অত্যন্ত গুরুত্বপূর্ণ।
- ডোমেইন: সফটওয়্যারে, 'ডোমেইন' হলো সেই নির্দিষ্ট বিষয় এলাকা যা ব্যবহারকারীর ব্যবসার অন্তর্গত। একটি ই-কমার্স অ্যাপ্লিকেশনের জন্য, ডোমেইনের মধ্যে পণ্য (Products), গ্রাহক (Customers), অর্ডার (Orders) এবং পেমেন্ট (Payments)-এর মতো ধারণাগুলো অন্তর্ভুক্ত। একটি সোশ্যাল মিডিয়া প্ল্যাটফর্মের জন্য, এর মধ্যে ব্যবহারকারী (Users), পোস্ট (Posts), মন্তব্য (Comments) এবং লাইক (Likes) অন্তর্ভুক্ত।
- ডোমেইন অবজেক্ট মডেলিং: এটি হলো সেই প্রক্রিয়া যার মাধ্যমে একটি সফটওয়্যার মডেল তৈরি করা হয় যা সেই বিজনেস ডোমেইনের মধ্যে থাকা এনটিটি, তাদের আচরণ এবং তাদের সম্পর্কগুলোকে উপস্থাপন করে। এটি বাস্তব জগতের ধারণাগুলোকে কোডে রূপান্তর করার একটি প্রক্রিয়া।
একটি ভালো ডোমেইন মডেল শুধুমাত্র ডেটা কন্টেইনারের সংগ্রহ নয়। এটি আপনার বিজনেস রুল বা ব্যবসায়িক নিয়মের একটি জীবন্ত উপস্থাপনা। একটি অর্ডার (Order) অবজেক্টের শুধু আইটেমের তালিকা থাকলেই চলবে না; এর জানা উচিত কীভাবে তার মোট মূল্য গণনা করতে হয়, কীভাবে একটি নতুন আইটেম যোগ করতে হয় এবং এটি বাতিল করা যাবে কিনা। ডেটা এবং আচরণের এই একত্রীকরণ (encapsulation) একটি স্থিতিশীল অ্যাপ্লিকেশন কোর তৈরির চাবিকাঠি।
সাধারণ সমস্যা: "মডেল" লেয়ারে বিশৃঙ্খলা
অনেক জাভাস্ক্রিপ্ট অ্যাপ্লিকেশনে, বিশেষ করে যেগুলো ধীরে ধীরে বড় হয়, 'মডেল' লেয়ারটি প্রায়শই একটি অবহেলিত বিষয় থাকে। আমরা প্রায়শই এই অ্যান্টি-প্যাটার্নটি দেখতে পাই:
// Somewhere in an API controller or service...
async function createUser(req, res) {
const { email, password, firstName, lastName } = req.body;
// Business logic and validation is scattered here
if (!email || !email.includes('@')) {
return res.status(400).send({ error: 'A valid email is required.' });
}
if (!password || password.length < 8) {
return res.status(400).send({ error: 'Password must be at least 8 characters.' });
}
const user = {
email: email.toLowerCase(),
password: await hashPassword(password), // Some utility function
fullName: `${firstName} ${lastName}`, // Logic for derived data is here
createdAt: new Date()
};
// Now, what is `user`? It's just a plain object.
// Nothing stops another developer from doing this later:
// user.email = 'an-invalid-email';
// user.password = 'short';
await db.users.insert(user);
res.status(201).send(user);
}
এই পদ্ধতিটি বেশ কয়েকটি গুরুতর সমস্যা তৈরি করে:
- সত্যের একক উৎসের অভাব (No Single Source of Truth): একটি বৈধ 'user' কী কী নিয়ে গঠিত, তার নিয়মগুলো এই একটি কন্ট্রোলারের মধ্যে সংজ্ঞায়িত করা হয়েছে। যদি সিস্টেমের অন্য কোনো অংশের একটি user তৈরি করার প্রয়োজন হয়? আপনি কি এই লজিকটি কপি-পেস্ট করবেন? এটি অসামঞ্জস্যতা এবং বাগের জন্ম দেয়।
- অ্যানিমিক ডোমেইন মডেল (Anemic Domain Model): `user` অবজেক্টটি কেবল একটি 'বোবা' ডেটার ব্যাগ। এর কোনো আচরণ বা আত্ম-সচেতনতা নেই। এর উপর কাজ করা সমস্ত লজিক বাইরে অবস্থান করে।
- নিম্ন সঙ্গতি (Low Cohesion): একজন ব্যবহারকারীর পুরো নাম তৈরি করার লজিকটি এপিআই রিকোয়েস্ট/রেসপন্স হ্যান্ডলিং এবং পাসওয়ার্ড হ্যাশিংয়ের সাথে মিশে আছে।
- পরীক্ষা করা কঠিন (Difficult to Test): ইউজার তৈরির লজিক পরীক্ষা করার জন্য, আপনাকে HTTP রিকোয়েস্ট এবং রেসপন্স, ডেটাবেস এবং হ্যাশিং ফাংশনগুলোকে মক (mock) করতে হবে। আপনি শুধু 'user' ধারণাটিকে বিচ্ছিন্নভাবে পরীক্ষা করতে পারবেন না।
- অব্যক্ত চুক্তি (Implicit Contracts): অ্যাপ্লিকেশনের বাকি অংশকে শুধু 'অনুমান' করতে হয় যে একজন ব্যবহারকারীকে প্রতিনিধিত্বকারী যেকোনো অবজেক্টের একটি নির্দিষ্ট গঠন রয়েছে এবং এর ডেটা বৈধ। এর কোনো নিশ্চয়তা নেই।
সমাধান: জাভাস্ক্রিপ্ট মডিউল এনটিটি প্যাটার্ন
মডিউল এনটিটি প্যাটার্ন একটি স্ট্যান্ডার্ড জাভাস্ক্রিপ্ট মডিউল (একটি ফাইল) ব্যবহার করে একটি একক ডোমেইন ধারণার সবকিছু সংজ্ঞায়িত করার মাধ্যমে এই সমস্যাগুলো সমাধান করে। এই মডিউলটি সেই এনটিটির জন্য সত্যের definitive উৎস হয়ে ওঠে।
একটি মডিউল এনটিটি সাধারণত একটি ফ্যাক্টরি ফাংশন (factory function) প্রকাশ করে। এই ফাংশনটি এনটিটির একটি বৈধ ইনস্ট্যান্স তৈরি করার জন্য দায়ী। এটি যে অবজেক্টটি রিটার্ন করে তা শুধু ডেটা নয়; এটি একটি সমৃদ্ধ ডোমেইন অবজেক্ট যা তার নিজস্ব ডেটা, ভ্যালিডেশন এবং বিজনেস লজিককে এনক্যাপসুলেট করে।
একটি মডিউল এনটিটির মূল বৈশিষ্ট্য
- এনক্যাপসুলেশন (Encapsulation): এটি ডেটা এবং সেই ডেটার উপর কাজ করা ফাংশনগুলোকে একসাথে বান্ডিল করে।
- সীমানায় ভ্যালিডেশন (Validation at the Boundary): এটি নিশ্চিত করে যে একটি অবৈধ এনটিটি তৈরি করা অসম্ভব। এটি তার নিজস্ব অবস্থাকে সুরক্ষিত রাখে।
- পরিষ্কার এপিআই (Clear API): এটি এনটিটির সাথে ইন্টারঅ্যাক্ট করার জন্য ফাংশনের একটি পরিষ্কার, ইচ্ছাকৃত সেট (একটি পাবলিক এপিআই) প্রকাশ করে, এবং অভ্যন্তরীণ বাস্তবায়নের বিবরণ লুকিয়ে রাখে।
- অপরিবর্তনীয়তা (Immutability): এটি প্রায়শই অপরিবর্তনীয় বা শুধুমাত্র পঠনযোগ্য (read-only) অবজেক্ট তৈরি করে যাতে দুর্ঘটনাজনিত স্টেট পরিবর্তন রোধ করা যায় এবং অনুমানযোগ্য আচরণ নিশ্চিত করা যায়।
- বহনযোগ্যতা (Portability): এটির ফ্রেমওয়ার্ক (যেমন Express, React) বা বাহ্যিক সিস্টেম (যেমন ডেটাবেস, এপিআই) এর উপর কোনো নির্ভরতা নেই। এটি বিশুদ্ধ বিজনেস লজিক।
একটি মডিউল এনটিটির মূল উপাদান
আসুন আমরা এই প্যাটার্ন ব্যবহার করে আমাদের `User` ধারণাটি পুনরায় তৈরি করি। আমরা একটি ফাইল, `user.js` (অথবা টাইপস্ক্রিপ্ট ব্যবহারকারীদের জন্য `user.ts`) তৈরি করব এবং এটি ধাপে ধাপে নির্মাণ করব।
১. ফ্যাক্টরি ফাংশন: আপনার অবজেক্ট কনস্ট্রাক্টর
ক্লাসের পরিবর্তে, আমরা একটি ফ্যাক্টরি ফাংশন (যেমন, `buildUser`) ব্যবহার করব। ফ্যাক্টরিগুলো দারুণ নমনীয়তা প্রদান করে, `this` কীওয়ার্ড নিয়ে ঝামেলা এড়ায় এবং জাভাস্ক্রিপ্টে প্রাইভেট স্টেট ও এনক্যাপসুলেশনকে আরও স্বাভাবিক করে তোলে।
আমাদের লক্ষ্য হলো এমন একটি ফাংশন তৈরি করা যা কাঁচা ডেটা নেয় এবং একটি সুগঠিত, নির্ভরযোগ্য User অবজেক্ট রিটার্ন করে।
// file: /domain/user.js
export default function buildMakeUser() {
// This inner function is the actual factory.
// It has access to any dependencies passed to buildMakeUser, if needed.
return function makeUser({
id = generateId(), // Let's assume a function to generate a unique ID
firstName,
lastName,
email,
passwordHash,
createdAt = new Date()
}) {
// ... validation and logic will go here ...
const user = {
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => email,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
};
// Using Object.freeze to make the object immutable.
return Object.freeze(user);
}
}
এখানে কয়েকটি জিনিস লক্ষ্য করুন। আমরা একটি ফাংশন ব্যবহার করছি যা একটি ফাংশন রিটার্ন করে (একটি হায়ার-অর্ডার ফাংশন)। এটি ডিপেন্ডেন্সি ইনজেক্ট করার জন্য একটি শক্তিশালী প্যাটার্ন, যেমন একটি ইউনিক আইডি জেনারেটর বা একটি ভ্যালিডেটর লাইব্রেরি, যা এনটিটিকে একটি নির্দিষ্ট বাস্তবায়নের সাথে সংযুক্ত না করেই করা যায়। আপাতত, আমরা এটিকে সহজ রাখব।
২. ডেটা ভ্যালিডেশন: প্রবেশদ্বারের প্রহরী
একটি এনটিটিকে অবশ্যই তার নিজস্ব অখণ্ডতা রক্ষা করতে হবে। একটি অবৈধ অবস্থায় `User` তৈরি করা অসম্ভব হওয়া উচিত। আমরা ফ্যাক্টরি ফাংশনের ভিতরেই ভ্যালিডেশন যোগ করব। যদি ডেটা অবৈধ হয়, ফ্যাক্টরিটি একটি এরর (error) থ্রো করবে এবং স্পষ্টভাবে জানাবে কী ভুল হয়েছে।
// file: /domain/user.js
export default function buildMakeUser({ Id, isValidEmail, hashPassword }) {
return function makeUser({
id = Id.makeId(),
firstName,
lastName,
email,
password, // We now take a plain password and handle it inside
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('User must have a valid id.');
}
if (!firstName || firstName.length < 2) {
throw new Error('First name must be at least 2 characters long.');
}
if (!lastName || lastName.length < 2) {
throw new Error('Last name must be at least 2 characters long.');
}
if (!email || !isValidEmail(email)) {
throw new Error('User must have a valid email address.');
}
if (!password || password.length < 8) {
throw new Error('Password must be at least 8 characters long.');
}
// Data normalization and transformation happens here
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
});
}
}
এখন, আমাদের সিস্টেমের যেকোনো অংশ যা একটি `User` তৈরি করতে চায়, তাকে অবশ্যই এই ফ্যাক্টরির মধ্য দিয়ে যেতে হবে। আমরা প্রতিবার নিশ্চিত ভ্যালিডেশন পাই। আমরা পাসওয়ার্ড হ্যাশ করা এবং ইমেল ঠিকানা নরম্যালাইজ করার লজিকটিকেও এনক্যাপসুলেট করেছি। অ্যাপ্লিকেশনের বাকি অংশকে এই বিবরণগুলো সম্পর্কে জানতে বা চিন্তা করতে হবে না।
৩. বিজনেস লজিক: আচরণকে এনক্যাপসুলেট করা
আমাদের `User` অবজেক্টটি এখনও কিছুটা অ্যানিমিক। এটি ডেটা ধারণ করে, কিন্তু এটি কিছু *করে* না। আসুন আমরা আচরণ যোগ করি—এমন মেথড যা ডোমেইন-নির্দিষ্ট ক্রিয়াগুলোকে প্রতিনিধিত্ব করে।
// ... inside the makeUser function ...
if (!password || password.length < 8) {
// ...
}
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt,
// Business Logic / Behavior
getFullName: () => `${firstName} ${lastName}`,
// A method that describes a business rule
canVote: () => {
// In some countries, voting age is 18. This is a business rule.
// Let's assume we have a dateOfBirth property.
const age = calculateAge(dateOfBirth);
return age >= 18;
}
});
// ...
`getFullName` লজিকটি আর কোনো এলোমেলো কন্ট্রোলারে ছড়ানো-ছিটানো নেই; এটি `User` এনটিটির নিজের অংশ। এখন যার কাছেই একটি `User` অবজেক্ট আছে, সে নির্ভরযোগ্যভাবে `user.getFullName()` কল করে পুরো নাম পেতে পারে। লজিকটি একবার, এক জায়গায় সংজ্ঞায়িত করা হয়েছে।
একটি বাস্তব উদাহরণ তৈরি: একটি সাধারণ ই-কমার্স সিস্টেম
আসুন এই প্যাটার্নটি আরও একটি আন্তঃসংযুক্ত ডোমেইনে প্রয়োগ করি। আমরা একটি `Product`, একটি `OrderItem`, এবং একটি `Order` মডেল করব।
১. `Product` এনটিটি মডেলিং
একটি পণ্যের একটি নাম, একটি মূল্য এবং কিছু স্টকের তথ্য থাকে। এটির একটি নাম থাকতে হবে এবং এর মূল্য ঋণাত্মক হতে পারবে না।
// file: /domain/product.js
export default function buildMakeProduct({ Id }) {
return function makeProduct({
id = Id.makeId(),
name,
description,
price,
stock = 0
}) {
if (!Id.isValidId(id)) {
throw new Error('Product must have a valid ID.');
}
if (!name || name.trim().length < 2) {
throw new Error('Product name must be at least 2 characters.');
}
if (isNaN(price) || price <= 0) {
throw new Error('Product must have a price greater than zero.');
}
if (isNaN(stock) || stock < 0) {
throw new Error('Stock must be a non-negative number.');
}
return Object.freeze({
getId: () => id,
getName: () => name,
getDescription: () => description,
getPrice: () => price,
getStock: () => stock,
// Business logic
isAvailable: () => stock > 0,
// A method that modifies state by returning a new instance
reduceStock: (amount) => {
if (amount > stock) {
throw new Error('Not enough stock available.');
}
// Return a NEW product object with the updated stock
return makeProduct({ id, name, description, price, stock: stock - amount });
}
});
}
}
`reduceStock` মেথডটি লক্ষ্য করুন। এটি অপরিবর্তনীয়তার সাথে সম্পর্কিত একটি গুরুত্বপূর্ণ ধারণা। বিদ্যমান অবজেক্টের `stock` প্রপার্টি পরিবর্তন করার পরিবর্তে, এটি আপডেট করা মানসহ একটি *নতুন* `Product` ইনস্ট্যান্স রিটার্ন করে। এটি স্টেট পরিবর্তনকে সুস্পষ্ট এবং অনুমানযোগ্য করে তোলে।
২. `Order` এনটিটি মডেলিং (অ্যাগ্রিগেট রুট)
একটি `Order` আরও জটিল। এটি ডোমেইন-ড্রিভেন ডিজাইন (DDD)-এ "অ্যাগ্রিগেট রুট" (Aggregate Root) নামে পরিচিত। এটি এমন একটি এনটিটি যা তার সীমানার মধ্যে অন্যান্য ছোট অবজেক্ট পরিচালনা করে। একটি `Order`-এ `OrderItem`-এর একটি তালিকা থাকে। আপনি সরাসরি একটি অর্ডার-এ পণ্য যোগ করেন না; আপনি একটি `OrderItem` যোগ করেন যাতে একটি পণ্য এবং তার পরিমাণ থাকে।
// file: /domain/order.js
export const ORDER_STATUS = {
PENDING: 'PENDING',
PAID: 'PAID',
SHIPPED: 'SHIPPED',
CANCELLED: 'CANCELLED'
};
export default function buildMakeOrder({ Id, validateOrderItem }) {
return function makeOrder({
id = Id.makeId(),
customerId,
items = [],
status = ORDER_STATUS.PENDING,
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('Order must have a valid ID.');
}
if (!customerId) {
throw new Error('Order must have a customer ID.');
}
let orderItems = [...items]; // Create a private copy to manage
return Object.freeze({
getId: () => id,
getCustomerId: () => customerId,
getItems: () => [...orderItems], // Return a copy to prevent external modification
getStatus: () => status,
getCreatedAt: () => createdAt,
// Business Logic
calculateTotal: () => {
return orderItems.reduce((total, item) => {
return total + (item.getPrice() * item.getQuantity());
}, 0);
},
addItem: (item) => {
// validateOrderItem is a function that ensures the item is a valid OrderItem entity
validateOrderItem(item);
// Business rule: prevent adding duplicates, just increase quantity
const existingItemIndex = orderItems.findIndex(i => i.getProductId() === item.getProductId());
if (existingItemIndex > -1) {
const newQuantity = orderItems[existingItemIndex].getQuantity() + item.getQuantity();
// Here you'd update the quantity on the existing item
// (This requires items to be mutable or have an update method)
} else {
orderItems.push(item);
}
},
markPaid: () => {
if (status !== ORDER_STATUS.PENDING) {
throw new Error('Only pending orders can be marked as paid.');
}
// Return a new Order instance with the updated status
return makeOrder({ id, customerId, items: orderItems, status: ORDER_STATUS.PAID, createdAt });
}
});
}
}
এই `Order` এনটিটি এখন জটিল ব্যবসায়িক নিয়ম প্রয়োগ করে:
- এটি তার নিজস্ব আইটেমের তালিকা পরিচালনা করে।
- এটি তার নিজস্ব মোট মূল্য গণনা করতে জানে।
- এটি স্টেট ট্রানজিশন (state transition) প্রয়োগ করে (যেমন, আপনি শুধুমাত্র একটি `PENDING` অর্ডারকে `PAID` হিসাবে চিহ্নিত করতে পারেন)।
অর্ডারের জন্য বিজনেস লজিক এখন এই মডিউলের মধ্যে সুন্দরভাবে এনক্যাপসুলেট করা, বিচ্ছিন্নভাবে পরীক্ষাযোগ্য এবং আপনার সম্পূর্ণ অ্যাপ্লিকেশন জুড়ে পুনঃব্যবহারযোগ্য।
উন্নত প্যাটার্ন এবং বিবেচ্য বিষয়
অপরিবর্তনীয়তা: পূর্বাভাসের ভিত্তিপ্রস্তর
আমরা অপরিবর্তনীয়তা নিয়ে আলোচনা করেছি। এটি এত গুরুত্বপূর্ণ কেন? যখন অবজেক্টগুলো অপরিবর্তনীয় হয়, তখন আপনি সেগুলোকে আপনার অ্যাপ্লিকেশনের মধ্যে নিশ্চিন্তে পাস করতে পারেন, এই ভয় ছাড়াই যে কোনো দূরবর্তী ফাংশন অপ্রত্যাশিতভাবে তাদের স্টেট পরিবর্তন করে ফেলবে। এটি একটি পুরো শ্রেণীর বাগ দূর করে এবং আপনার অ্যাপ্লিকেশনের ডেটা ফ্লো বোঝা অনেক সহজ করে তোলে।
Object.freeze() একটি শ্যালো ফ্রিজ (shallow freeze) প্রদান করে। নেস্টেড অবজেক্ট বা অ্যারেসহ এনটিটিগুলোর জন্য (যেমন আমাদের `Order`), আপনাকে আরও সতর্ক হতে হবে। উদাহরণস্বরূপ, `order.getItems()`-এ, আমরা একটি কপি (`[...orderItems]`) রিটার্ন করেছি যাতে কলার সরাসরি অর্ডারের অভ্যন্তরীণ অ্যারেতে আইটেম পুশ করতে না পারে।
জটিল অ্যাপ্লিকেশনগুলোর জন্য, Immer-এর মতো লাইব্রেরিগুলো অপরিবর্তনীয় নেস্টেড স্ট্রাকচারের সাথে কাজ করা অনেক সহজ করে তুলতে পারে, কিন্তু মূল নীতিটি একই থাকে: আপনার এনটিটিগুলোকে অপরিবর্তনীয় মান হিসাবে বিবেচনা করুন। যখন কোনো পরিবর্তন করার প্রয়োজন হয়, তখন একটি নতুন মান তৈরি করুন।
অ্যাসিঙ্ক্রোনাস অপারেশন এবং পারসিস্টেন্স পরিচালনা
আপনি হয়তো লক্ষ্য করেছেন আমাদের এনটিটিগুলো সম্পূর্ণ সিঙ্ক্রোনাস। তারা ডেটাবেস বা এপিআই সম্পর্কে কিছুই জানে না। এটি ইচ্ছাকৃত এবং এই প্যাটার্নের একটি প্রধান শক্তি!
এনটিটিদের নিজেদের সেভ করা উচিত নয়। একটি এনটিটির কাজ হলো ব্যবসায়িক নিয়ম প্রয়োগ করা। ডেটাবেসে ডেটা সংরক্ষণ করার কাজটি আপনার অ্যাপ্লিকেশনের একটি ভিন্ন লেয়ারের, যা প্রায়শই সার্ভিস লেয়ার (Service Layer), ইউজ কেস লেয়ার (Use Case Layer), বা রিপোজিটরি প্যাটার্ন (Repository Pattern) নামে পরিচিত।
এখানে তারা কীভাবে ইন্টারঅ্যাক্ট করে তা দেখানো হলো:
// file: /use-cases/create-user.js
// This use case depends on the user entity factory and a database access function.
export default function makeCreateUser({ makeUser, usersDatabase }) {
return async function createUser(userInfo) {
// 1. Create a valid domain entity. This step validates the data.
const user = makeUser(userInfo);
// 2. Check for business rules that require external data (e.g., email uniqueness)
const exists = await usersDatabase.findByEmail({ email: user.getEmail() });
if (exists) {
throw new Error('Email address is already in use.');
}
// 3. Persist the entity. The database needs a plain object.
const persisted = await usersDatabase.insert({
id: user.getId(),
firstName: user.getFirstName(),
// ... and so on
});
return persisted;
}
}
এই সেপারেশন অফ কনসার্নস (separation of concerns) শক্তিশালী:
- `User` এনটিটি বিশুদ্ধ, সিঙ্ক্রোনাস এবং ইউনিট টেস্ট করা সহজ।
- `createUser` ইউজ কেসটি অর্কেস্ট্রেশনের জন্য দায়ী এবং একটি মক ডেটাবেস দিয়ে ইন্টিগ্রেশন-টেস্ট করা যেতে পারে।
- `usersDatabase` মডিউলটি নির্দিষ্ট ডেটাবেস প্রযুক্তির জন্য দায়ী এবং এটি আলাদাভাবে পরীক্ষা করা যেতে পারে।
সিরিয়ালাইজেশন এবং ডিসিরিয়ালাইজেশন
আপনার এনটিটিগুলো, তাদের মেথডসহ, সমৃদ্ধ অবজেক্ট। কিন্তু যখন আপনি নেটওয়ার্কের মাধ্যমে ডেটা পাঠান (যেমন, একটি JSON API রেসপন্সে) বা ডেটাবেসে সংরক্ষণ করেন, তখন আপনার একটি প্লেইন ডেটা উপস্থাপনা প্রয়োজন। এই প্রক্রিয়াটিকে সিরিয়ালাইজেশন (serialization) বলা হয়।
একটি সাধারণ প্যাটার্ন হলো আপনার এনটিটিতে একটি `toJSON()` বা `toObject()` মেথড যোগ করা।
// ... inside the makeUser function ...
return Object.freeze({
getId: () => id,
// ... other getters
// Serialization method
toObject: () => ({
id,
firstName,
lastName,
email: normalizedEmail,
createdAt
// Notice we don't include the passwordHash
})
});
বিপরীত প্রক্রিয়া, অর্থাৎ ডেটাবেস বা এপিআই থেকে প্লেইন ডেটা নিয়ে সেটিকে আবার একটি সমৃদ্ধ ডোমেইন এনটিটিতে পরিণত করা, ঠিক এই কাজটিই আপনার `makeUser` ফ্যাক্টরি ফাংশনটি করে। এটি হলো ডিসিরিয়ালাইজেশন (deserialization)।
টাইপস্ক্রিপ্ট বা JSDoc দিয়ে টাইপিং
যদিও এই প্যাটার্নটি ভ্যানিলা জাভাস্ক্রিপ্টে পুরোপুরি কাজ করে, টাইপস্ক্রিপ্ট বা JSDoc দিয়ে স্ট্যাটিক টাইপ যোগ করলে এটি আরও শক্তিশালী হয়। টাইপগুলো আপনাকে আপনার এনটিটির 'আকৃতি' আনুষ্ঠানিকভাবে সংজ্ঞায়িত করতে দেয়, যা চমৎকার অটোকমপ্লিশন এবং কম্পাইল-টাইম চেক প্রদান করে।
// file: /domain/user.ts
// Define the entity's public interface
export type User = Readonly<{
getId: () => string;
getFirstName: () => string;
// ... etc
getFullName: () => string;
}>;
// The factory function now returns the User type
export default function buildMakeUser(...) {
return function makeUser(...): User {
// ... implementation
}
}
মডিউল এনটিটি প্যাটার্নের সামগ্রিক সুবিধা
এই প্যাটার্নটি গ্রহণ করার মাধ্যমে, আপনি এমন অনেক সুবিধা অর্জন করেন যা আপনার অ্যাপ্লিকেশন বড় হওয়ার সাথে সাথে বৃদ্ধি পায়:
- সত্যের একক উৎস (Single Source of Truth): ব্যবসায়িক নিয়ম এবং ডেটা ভ্যালিডেশন কেন্দ্রীভূত এবং দ্ব্যর্থহীন। একটি নিয়মের পরিবর্তন ঠিক একটি জায়গাতেই করা হয়।
- উচ্চ সঙ্গতি, নিম্ন কাপলিং (High Cohesion, Low Coupling): এনটিটিগুলো স্বয়ংসম্পূর্ণ এবং বাইরের সিস্টেমের উপর নির্ভর করে না। এটি আপনার কোডবেসকে মডিউলার এবং রিফ্যাক্টর করা সহজ করে তোলে।
- সর্বোচ্চ পরীক্ষাযোগ্যতা (Supreme Testability): আপনি পুরো বিশ্বকে মক না করেই আপনার সবচেয়ে গুরুত্বপূর্ণ বিজনেস লজিকের জন্য সহজ এবং দ্রুত ইউনিট টেস্ট লিখতে পারেন।
- উন্নত ডেভেলপার অভিজ্ঞতা (Improved Developer Experience): যখন একজন ডেভেলপারকে একটি `User` নিয়ে কাজ করতে হয়, তখন তার ব্যবহারের জন্য একটি পরিষ্কার, অনুমানযোগ্য এবং স্ব-ডকুমেন্টিং এপিআই থাকে। প্লেইন অবজেক্টের আকৃতি নিয়ে আর অনুমান করতে হয় না।
- স্কেলেবিলিটির ভিত্তি (A Foundation for Scalability): এই প্যাটার্নটি আপনাকে একটি স্থিতিশীল, নির্ভরযোগ্য কোর দেয়। আপনি যখন আরও ফিচার, ফ্রেমওয়ার্ক বা UI কম্পোনেন্ট যোগ করেন, তখন আপনার বিজনেস লজিক সুরক্ষিত এবং সামঞ্জস্যপূর্ণ থাকে।
উপসংহার: আপনার অ্যাপ্লিকেশনের জন্য একটি শক্ত ভিত্তি তৈরি করুন
দ্রুত পরিবর্তনশীল ফ্রেমওয়ার্ক এবং লাইব্রেরির জগতে, এটা ভুলে যাওয়া সহজ যে এই টুলগুলো ক্ষণস্থায়ী। এগুলো পরিবর্তিত হবে। যা টিকে থাকে তা হলো আপনার বিজনেস ডোমেইনের মূল লজিক। এই ডোমেইনটিকে সঠিকভাবে মডেল করার জন্য সময় বিনিয়োগ করা শুধু একটি অ্যাকাডেমিক অনুশীলন নয়; এটি আপনার সফটওয়্যারের স্বাস্থ্য এবং দীর্ঘায়ুর জন্য আপনি করতে পারেন এমন সবচেয়ে গুরুত্বপূর্ণ দীর্ঘমেয়াদী বিনিয়োগগুলোর মধ্যে একটি।
জাভাস্ক্রিপ্ট মডিউল এনটিটি প্যাটার্ন এই ধারণাগুলো বাস্তবায়নের জন্য একটি সহজ, শক্তিশালী এবং নেটিভ উপায় সরবরাহ করে। এর জন্য কোনো ভারী ফ্রেমওয়ার্ক বা জটিল সেটআপের প্রয়োজন নেই। এটি ভাষার মৌলিক বৈশিষ্ট্যগুলো—মডিউল, ফাংশন এবং ক্লোজার—ব্যবহার করে আপনাকে আপনার অ্যাপ্লিকেশনের জন্য একটি পরিষ্কার, স্থিতিশীল এবং বোধগম্য কোর তৈরি করতে সহায়তা করে। আপনার পরবর্তী প্রজেক্টে একটি মূল এনটিটি দিয়ে শুরু করুন। এর বৈশিষ্ট্যগুলো মডেল করুন, এর তৈরিকে যাচাই করুন এবং একে আচরণ দিন। আপনি আরও একটি শক্তিশালী এবং পেশাদার সফটওয়্যার আর্কিটেকচারের দিকে প্রথম পদক্ষেপ নেবেন।